package org.upfrost.mapping;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.upfrost.Mapper;
import org.upfrost.util.StringUtils;


/**
 * A Mapping is an association between a sql column and a javabean-compliant object property.
 * 
 * @author Rodrigo Reyes
 *
 */
public class Mapping<T> 
{
	private String m_sqlName;

	private Field m_field = null;
	private PropertyDescriptor m_descriptor = null;
	private Class<T> m_type;
	private String m_name;
	private boolean m_transient = false;
	private boolean m_key = false;
	private boolean m_keyGenerated = false;
	private Mapper m_mapper;
	
	/**
	 * Create a Mapping object for an object field
	 * @param clazz
	 * @param f
	 * @param mapper
	 * @throws IntrospectionException
	 */
	public Mapping(Class<?> clazz, Field f, Mapper mapper) throws IntrospectionException
	{
		m_mapper = mapper;
		m_type = (Class<T>) f.getType();
		m_field = f;
		m_field.setAccessible(true);
		m_name = f.getName();
		m_sqlName = f.getName().toLowerCase();
	}
	
	/**
	 * Create a Mapping object for an object bean property accessed by a method 
 	 * @param clazz
	 * @param propName
	 * @param type
	 * @param mapper
	 * @throws IntrospectionException
	 */
	public Mapping(Class<?> clazz, String propName, Class<T> type, Mapper mapper) throws IntrospectionException
	{
		m_mapper = mapper;
		m_type = type;
		try {
			m_descriptor = new PropertyDescriptor(propName, clazz);
			m_name = propName;
			m_sqlName = propName.toLowerCase();
		} catch (Exception exc)
		{
			exc.printStackTrace();
		}
	}

	/**
	 * The name of the SQL column this mapping is associated to
	 * @return
	 */
	public String getSqlName() {
		return m_sqlName;
	}

	public void setSqlName(String mSqlName) {
		m_sqlName = mSqlName;
	}

	/**
	 * The name of the javabean property
	 * @return
	 */
	public String getName()
	{
		return m_name;
	}

	/**
	 * Test wether this mapping is operational, it needs to have a sql column name and either a field or a method accessor for the property.  
	 * @return true if the mapping is operational
	 */
	public boolean isValid()
	{
		return (m_name!=null) && (m_field!=null || m_descriptor!= null);
	}

	/**
	 * Get the java object property managed by this mapping
	 * @param instance
	 * @return
	 */
	public Object get(Object instance)
	{
		if (m_field != null)
		{
			try {
				return m_field.get(instance);
			} catch (Exception exc)
			{
				throw new RuntimeException("Can't get value from field of " + this.toString(), exc);
			}
		}

		if (m_descriptor != null)
		{
			Method read = m_descriptor.getReadMethod();
			try {
				return read.invoke(instance);
			}
			catch (Exception exc)
			{
				throw new RuntimeException("Can't get value from accessor of " + this.toString(), exc);				
			}
		}

		throw new RuntimeException("Can't retrieve value from " + this + ", no accessor or field?");
	}

	/**
	 * Set the java object property managed by this mapping
	 * @param instance
	 * @param value
	 */
	public void set(Object instance, Object value) 
	{
		value = m_mapper.convertTypeIfNeeded(value, this.m_type);
		
		if (m_field != null)
		{
			try {
				m_field.set(instance, value);
			} catch (Exception exc)
			{
				throw new RuntimeException("Can't set value " + value + " to " + m_field, exc);
			}
			return;
		}
		else if (m_descriptor != null)
		{
			Method write = m_descriptor.getWriteMethod();
			try {
				write.invoke(instance, value);
			} catch (Exception e) {
				e.printStackTrace();
				throw new RuntimeException("Impossible to write value " + value + " ("+value.getClass().getCanonicalName()+") to accessor " + m_descriptor.getName() + " in class " + instance.getClass().getCanonicalName() + " when expecting argument of type " + StringUtils.join(write.getParameterTypes(),"::"));
			}
		}
		else
			throw new RuntimeException("Invalid mapping " + this);
	}

	public Class<T> getType()
	{
		return m_type;
	}

	@Override
	public String toString() 
	{
		return "Mapping(" +(m_field!=null?m_field.getName():"") + (m_descriptor!=null?m_descriptor.getName():"")+ "::" + m_sqlName + ")";
	}

	public boolean isTransient() {
		return m_transient;
	}

	public void setTransient(boolean transient1) {
		m_transient = transient1;
	}

	@SuppressWarnings("unchecked")
	@Override
	public boolean equals(Object obj) {

		if (obj != null && obj instanceof Mapping<?>)
		{
			return this.m_name.equals(((Mapping<T>)obj).m_name);
		}
		
		return super.equals(obj);
	}

	@Override
	public int hashCode() {
		// TODO Auto-generated method stub
		return super.hashCode();
	}

	public boolean isKey() {
		return m_key;
	}

	public void setKey(boolean key) {
		m_key = key;
	}

	public boolean isKeyGenerated() {
		return m_keyGenerated;
	}

	public void setKeyGenerated(boolean keyGenerated) {
		m_keyGenerated = keyGenerated;
	}

	public boolean isAccessedByField()
	{
		return m_field != null;
	}
	
	public boolean isAccessedByAccessorMethod()
	{
		return m_descriptor != null;
	}
}
